開始看 React 專案後,我很常遇到這種程式:
useEffect(() => {
fetchData();
}, []);
第一次看到時,我會直接把它翻成:
「它在監聽什麼?」
因為以前學 JavaScript 時,看到:
addEventListener(...)
就是在監聽事件。
所以看到 useEffect,很自然也會往「監聽」的方向理解。
但後來才慢慢發現:
useEffect並不是單純拿來監聽使用者事件的。
像按鈕點擊,React 本來就有:
onClick
輸入內容改變也有:
onChange
useEffect 處理的是另外一類事情。
例如:
useEffect(() => {
fetchData();
}, []);
假設:
fetchData();
裡面會呼叫 API。
那整個流程可以先理解成:
Component 開始 Render
↓
React 把畫面更新完成
↓
useEffect 執行
↓
fetchData()
↓
呼叫 API
↓
取得資料
↓
更新 State
↓
再次 Render
所以你可能什麼都還沒按,
頁面一進來,Network 就已經出現 Request。
不是因為使用者觸發了按鈕。
而是這段:
useEffect(() => {
fetchData();
}, []);
在 Component 顯示之後執行了。
useEffect 可以先理解成什麼?我目前比較喜歡這樣理解:
當 React Render 完畫面後,如果還有一些事情需要跟 React 外部的東西同步,就可以使用 Effect。
例如:
呼叫 API
設定 Timer
訂閱事件
操作瀏覽器 API
跟第三方套件同步
這些事情不只是「計算 JSX 要長什麼樣子」。
它們會碰到 React Component 外面的世界。
所以 React 把這類事情稱為:
Side Effect
也就是「副作用」。
這個名字第一次看到很容易覺得很可怕。
其實先用一個簡單對比就好。
例如:
const fullName = firstName + lastName;
這只是根據現有資料算出另一個值。
沒有:
打 API
修改瀏覽器
設定 Timer
訂閱事件
這種事情通常不需要 useEffect。
但:
fetch("/api/users");
會對外送出 Request。
這就是跟 Component 外面的系統互動。
因此:
useEffect(() => {
fetch("/api/users");
}, []);
就比較符合 useEffect 的用途。
[] 是什麼?看到:
useEffect(() => {
fetchData();
}, []);
最容易讓人困惑的就是最後:
[]
這個叫做:
dependency array
也就是「依賴陣列」。
它是在告訴 React:
這個 Effect 依賴哪些值。
[]例如:
useEffect(() => {
fetchData();
}, []);
可以先簡化理解成:
Component 第一次完成 Render 後,執行這個 Effect。
所以:
進入頁面
↓
Render
↓
useEffect
↓
fetchData()
之後即使 Component 因為其他 State 更新而重新 Render,這個 Effect 不會因那些值而再次執行。
這也是為什麼很多「頁面第一次載入資料」的程式會長這樣。
例如:
useEffect(() => {
fetchUser(userId);
}, [userId]);
這時候它依賴:
userId
流程可以想成:
第一次 Render
↓
執行 Effect
↓
fetchUser(userId)
之後 userId 改變
↓
重新 Render
↓
Effect 再執行
↓
用新的 userId 重新取得資料
所以:
[userId]
不是說:
「一直盯著 userId 看。」
而是:
這個 Effect 使用了 userId,而且當 userId 改變時,需要重新同步一次。
假設:
const [userId, setUserId] = useState(1);
Effect:
useEffect(() => {
fetchUser(userId);
}, [userId]);
一開始:
userId = 1
↓
Render
↓
Effect
↓
GET /users/1
後來:
setUserId(2);
流程:
userId
1 → 2
↓
React Render
↓
React 發現 dependency 的 userId 改變
↓
Effect 再執行
↓
GET /users/2
這時候 useEffect 的存在就比較合理了。
因為:
外部 API 需要跟現在的
userId保持同步。
還可能看到:
useEffect(() => {
console.log("effect");
});
注意這裡沒有:
[]
這表示:
每次 Component Render 完之後,Effect 都會執行。
也就是:
Render
↓
Effect
State 改變
↓
Render
↓
Effect
又有 State 改變
↓
Render
↓
Effect
這時候就要特別小心。
如果 Effect 裡面又一直更新 State:
useEffect(() => {
setCount(count + 1);
});
就可能形成:
Render
↓
Effect
↓
setCount()
↓
Render
↓
Effect
↓
setCount()
↓
...
最後一直重複。
這也是為什麼看到 useEffect 時,dependency 是很重要的線索。
useEffect 不是「React 版 addEventListener」這裡我以前很容易混在一起。
例如 JavaScript:
button.addEventListener("click", handleClick);
React 通常直接是:
<button onClick={handleClick}>
Click
</button>
而不是:
useEffect(...)
所以:
使用者操作事件
→ onClick / onChange ...
Render 後需要同步外部系統
→ useEffect
這兩個概念要分開。
因為我們很常需要:
頁面顯示時,先取得資料。
例如:
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(data => {
setUsers(data);
});
}, []);
流程:
第一次 Render
↓
目前 users = []
↓
畫面先 Render
Render 完成
↓
useEffect 執行
↓
Request API
↓
Response 回來
↓
setUsers(data)
↓
State 改變
↓
第二次 Render
↓
畫面出現 users
所以頁面載入時,不是:
API 完全跑完
↓
才開始 Render
而比較像:
先 Render
↓
Effect 發 Request
↓
資料回來
↓
更新 State
↓
再 Render
這也是理解 React 頁面資料流時很重要的一條線。
例如看到:
useEffect(() => {
fetchRoot();
}, [showFilter]);
以前我的第一反應可能是:
useEffect到底是什麼?
現在我比較會先問:
這個 Effect 做什麼?
→ fetchRoot()
它依賴什麼?
→ showFilter
所以什麼情況會重新執行?
→ showFilter 改變
直接先得到:
第一次 Render
↓
fetchRoot()
showFilter 改變
↓
重新 Render
↓
fetchRoot() 再跑
再去看:
fetchRoot()
到底打哪支 API、資料回來放哪裡。
這樣就不用一開始被整段程式嚇到。
useEffect,我現在會問三件事比起背語法,我現在會先找:
① Effect 裡到底做了什麼?
② dependency array 裡有哪些值?
③ Effect 執行後,會不會更新 State?
因為這三件事通常就能幫助我畫出流程:
什麼條件
↓
觸發 Effect
↓
做什麼事情
↓
有沒有更新資料
↓
會不會再次 Render
useEffect實際專案裡,也很常使用其他封裝好的資料請求工具。
例如:
useRequest(...)
或其他 Data Fetching Library。
所以看到:
頁面載入
↓
打 API
不代表一定會找到:
useEffect
有些工具本身就幫忙處理了:
何時執行
loading
error
response
但不管使用什麼工具,我現在都會追同一件事:
這支 Request 到底是什麼時候被觸發的?
React Render
↓
畫面完成
如果還需要跟外部世界同步
↓
useEffect
例如:
├─ API
├─ Timer
├─ Subscription
└─ Browser API
然後 dependency:
[]
→ 第一次完成 Render 後執行
[userId]
→ 初次執行
→ userId 改變後再執行
沒有 dependency array
→ 每次 Render 後都會執行
所以以後看到:
useEffect(() => {
...
}, [...]);
我不會再只問:
「這段語法是什麼?」
而是會先問:
它在跟什麼東西同步?又是什麼變化讓它需要重新同步?
下一篇就可以正式進入我在真實專案裡更常遇到的資料請求方式:
useRequest 到底在幹嘛?API 是誰打出去的,Response 又去了哪裡?